﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Presentation;

namespace PowerPointRender
{
    /// <summary>
    /// Scan a PPTX file for sub-images - these are secret bread-crumbs left by others which we can
    /// then render in a "nice" way.
    /// </summary>
    class ExtractSubImages
    {
        /// <summary>
        /// All the meta data we extract from the presentation
        /// </summary>
        public class SlideSubInfo
        {
            public int slideSizeX, slideSizeY;

            public struct SubRenderInfo
            {
                public int slideNumber;
                public long posX, posY, width, height;
                public string[] url;
            }
            public SubRenderInfo[] renderings;
        }

        /// <summary>
        /// Search the given pptx file for everything we can find. If we can't open this as a pptx file
        /// then we return nothing (null).
        /// </summary>
        /// <returns>All the sub-renderings requested</returns>
        /// <param name="pptFile">The pptx file to look at.</param>
        public static SlideSubInfo FindAllPossibleRenderRequests(FileInfo pptFile, IErrorLogger logger)
        {
            SlideSubInfo nullResult = new SlideSubInfo() { renderings = new SlideSubInfo.SubRenderInfo[0] };

            //
            // Some quick x-checks
            //

            if (pptFile == null)
                return nullResult;
            if (pptFile.Extension != ".pptx")
                return nullResult;

            //
            // Now parse. If anything goes wrong, then we are outta here!
            //

            try
            {

                using (var ppt = PresentationDocument.Open(pptFile.FullName, false))
                {
                    Console.WriteLine("Opened!");
                    var ppart = ppt.PresentationPart;

                    //
                    // Get the slide size -  we will need this to be able to reference things down the line!
                    //

                    int xsize = 0;
                    int ysize = 0;
                    var presentation = ppart.Presentation;
                    var sizeInfo = presentation.GetFirstChild<SlideSize>();
                    if (sizeInfo != null)
                    {
                        xsize = sizeInfo.Cx;
                        ysize = sizeInfo.Cy;
                    }

                    //
                    // Ok, we need to look at every single slide.
                    //

                    var slidesIds = ppart.Presentation.SlideIdList.ChildElements.Cast<SlideId>().Select(s => s.RelationshipId);
                    var slides = slidesIds.Select(rid => ppart.GetPartById(rid)).Where(s => s is SlidePart).Cast<SlidePart>().Where(s => s != null);
                    var countedslides = slides.Zip(Enumerable.Range(1, 10000), (s, cnt) => new { SlidePart = s, SlideNumber = cnt }); // Slide numbers needed here!

                    //
                    // Now, some shapes might be at the root level, so get a stream of those shapes.
                    //

                    var renderTopLevelRequests = from sp in countedslides
                                                 let shapes = sp.SlidePart.Slide.CommonSlideData.ShapeTree
                                                 from r in GetRenderItems(shapes.Where(s => s is Shape).Cast<Shape>(), sp.SlideNumber)
                                                 select r;

                    //
                    // Now, look for groups and extract those
                    //

                    var rendersInGroupRequests = from sp in countedslides
                                                 let shapes = sp.SlidePart.Slide.CommonSlideData.ShapeTree
                                                 from r in GetGroupRenderItems(shapes.Where(s => s is GroupShape).Cast<GroupShape>(), sp.SlideNumber)
                                                 select r;



                    //
                    // Create the return item. If there are no renderings then we just return null.
                    //

                    var allrequests = renderTopLevelRequests.Concat(rendersInGroupRequests);
                    var result = new SlideSubInfo() { slideSizeX = xsize, slideSizeY = ysize, renderings = allrequests.ToArray() };
                    if (result.renderings.Length == 0)
                        return nullResult;
                    return result;
                }
            }
            catch (Exception e)
            {
                logger.LogMessage("RenderUsingPowerPoint", string.Format("Error parsing pptx file for text info on renderings '{0}'.", pptFile.FullName), e);
            }

            return nullResult;
        }

        /// <summary>
        /// Given a group, inspect it for shapes inside it (and groups) that might have a render item we are interseted in!
        /// </summary>
        /// <param name="iEnumerable"></param>
        /// <param name="slideNumber"></param>
        /// <returns></returns>
        private static IEnumerable<SlideSubInfo.SubRenderInfo> GetGroupRenderItems(IEnumerable<GroupShape> groups, int slideNumber, GroupTransformInfo trans = null)
        {
            if (trans == null)
                trans = new GroupTransformInfo();

            //
            // Extract the info from this group for transforms
            //

            var groupShapes = from g in groups
                              let transformInfo = ExtractGroupInfo(g, trans)
                              where g != null
                              from r in GetRenderItems(g.Where(s => s is Shape).Cast<Shape>(), slideNumber, transformInfo)
                              select r;

            var subGruops = from g in groups
                            let transformInfo = ExtractGroupInfo(g, trans)
                            where g != null
                            from sr in GetGroupRenderItems(g.Where(s => s is GroupShape).Cast<GroupShape>(), slideNumber, transformInfo)
                            select sr;

            foreach (var sinfo in groupShapes.Concat(subGruops))
            {
                yield return sinfo;
            }
        }

        class GroupTransformInfo
        {
            public long origX;
            public long origY;

            public long newX;
            public long newY;

            public double scaleX;
            public double scaleY;

            public GroupTransformInfo nested;

            public GroupTransformInfo()
            {
                origX = 0;
                origY = 0;
                newX = 0;
                newY = 0;
                scaleX = 1.0;
                scaleY = 1.0;
                nested = null;
            }

            public long ScaleWidth(long width)
            {
                var other = width;
                if (nested != null)
                    other = nested.ScaleWidth(width);

                return (long)(other * scaleX);
            }
            public long ScaleHeight(long height)
            {
                var other = height;
                if (nested != null)
                    other = nested.ScaleHeight(height);

                return (long)(other * scaleY);
            }

            public long TranslateX(long x)
            {
                var other = x;
                if (nested != null)
                    other = nested.TranslateX(other);

                double wrt = other - origX;
                wrt *= scaleX;
                return newX + (long)wrt;
            }

            public long TranslateY(long y)
            {
                var other = y;
                if (nested != null)
                    other = nested.TranslateY(other);

                double wrt = other - origY;
                wrt *= scaleY;
                return newY + (long)wrt;
            }
        }

        /// <summary>
        /// Get out the scaling info from this group thing.
        /// </summary>
        /// <param name="g"></param>
        /// <returns></returns>
        static GroupTransformInfo ExtractGroupInfo(GroupShape g, GroupTransformInfo enclosing)
        {
            var shapeInfo = g.GetFirstChild<GroupShapeProperties>();
            if (shapeInfo != null)
            {
                var tgroup = shapeInfo.GetFirstChild<DocumentFormat.OpenXml.Drawing.TransformGroup>();
                if (tgroup != null)
                {
                    var extents = tgroup.GetFirstChild<DocumentFormat.OpenXml.Drawing.Extents>();
                    var offsets = tgroup.GetFirstChild<DocumentFormat.OpenXml.Drawing.Offset>();
                    var childExt = tgroup.GetFirstChild<DocumentFormat.OpenXml.Drawing.ChildExtents>();
                    var childOff = tgroup.GetFirstChild<DocumentFormat.OpenXml.Drawing.ChildOffset>();

                    double scalingX = ((double)extents.Cx) / ((double)childExt.Cx);
                    double scalingY = ((double)extents.Cy) / ((double)childExt.Cy);

                    return new GroupTransformInfo() { scaleX = scalingX, scaleY = scalingY, origX = childOff.X, origY = childOff.Y, newX = offsets.X, newY = offsets.Y, nested = enclosing };
                }
            }
            return new GroupTransformInfo();
        }

        /// <summary>
        /// Look through a list of shapes and extract any render information in the shapes.
        /// </summary>
        /// <param name="iEnumerable"></param>
        /// <returns></returns>
        private static IEnumerable<SlideSubInfo.SubRenderInfo> GetRenderItems(IEnumerable<Shape> shapeList, int slideNumber, GroupTransformInfo trans = null)
        {
            if (trans == null)
                trans = new GroupTransformInfo();

            var allRendersFound = from shape in shapeList
                                  where shape != null
                                  let basicProperties = shape.GetFirstChild<NonVisualShapeProperties>()
                                  where basicProperties != null
                                  let drawingProp = basicProperties.GetFirstChild<DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties>()
                                  where drawingProp != null && drawingProp.Description != null && !string.IsNullOrWhiteSpace(drawingProp.Description.Value)
                                  let goodUrls = (from line in drawingProp.Description.Value.Split('\n')
                                                  where !string.IsNullOrWhiteSpace(line) && line.StartsWith("render:")
                                                  select line.Substring(7)).ToArray()
                                  where goodUrls.Length > 0
                                  let positionInfo = GetShapeLocation(shape)
                                  select new SlideSubInfo.SubRenderInfo()
                                  {
                                      slideNumber = slideNumber,
                                      url = goodUrls,
                                      posX = trans.TranslateX(positionInfo.Item1),
                                      posY = trans.TranslateY(positionInfo.Item2),
                                      width = trans.ScaleWidth(positionInfo.Item3),
                                      height = trans.ScaleHeight(positionInfo.Item4)
                                  };

            foreach (var sh in allRendersFound)
            {
                yield return sh;
            }
        }

        /// <summary>
        /// Returns the location of the shape in slide coordinates (assuming that things are not netsted in this presentation!!).
        /// </summary>
        /// <param name="shape"></param>
        /// <returns></returns>
        private static Tuple<long, long, long, long> GetShapeLocation(Shape shape)
        {
            var prop = shape.GetFirstChild<ShapeProperties>();
            if (prop == null)
                return Tuple.Create(0L, 0L, 0L, 0L);

            var transform = prop.GetFirstChild<DocumentFormat.OpenXml.Drawing.Transform2D>();
            if (transform == null)
                return Tuple.Create(0L, 0L, 0L, 0L);

            var offset = transform.GetFirstChild<DocumentFormat.OpenXml.Drawing.Offset>();
            var extent = transform.GetFirstChild<DocumentFormat.OpenXml.Drawing.Extents>();

            if (offset == null || extent == null)
                return Tuple.Create(0L, 0L, 0L, 0L);

            return Tuple.Create<long, long, long, long>(offset.X, offset.Y, extent.Cx, extent.Cy);
        }
    }
}
